react router英文官网:https://reactrouter.com/en/main

1.初始化项目

祝大家:开心学习,快乐生活,过好每一天~

1.1基于Vite创建React+TS项目

1、打开终端,运行下面的命令创建React+TS项目:

2、cd到项目根目录中

3、运行npm i命令,安装依赖包

4、运行npm run dev命令,启动项目

5、查看项目运行效果

1.2初始化项目

整套教程其实是react router官网的tutorial的改写,加了部分注释和typescript,所以下面的很多代码可以在这里找到:https://reactrouter.com/en/main/start/tutorial。比如说下面要用到的CSS和js代码,就可以直接复制粘贴。

image-20240112121215609

那是不是这门课买错了呢?没有买错,谁叫我看不懂英文文档的,中文文档我也提炼不出好的知识点。买这门课绝对没有错。

1、复制/粘贴教程所需的CSS样式src/index.css中。

代码很多,直接复制即可,这里就不粘贴出来了。

2、复制/粘贴教程所需的Data数据src/contacts.js中。在我们的项目中,老师建议使用老师改造好的TS类型的数据模块,复制/粘贴下面的代码到src/contacts.ts中。

src/vite-env.d.ts模块中,新增ContactType类型定义:

3、安装相关的依赖包:

4、删除src/App.tsxsrc/App.css文件

5、修改src/main.tsx文件,删除App组件相关的代码:

完成后效果:

image-20240113143355916

 

1.3添加Browser Router路由

1、在src/main.tsx中,按需导入createBrowserRouter函数和RouterProvider组件:

2、创建browser router对象:

3、基于RouterProvider组件的router属性,配置项目的路由:

效果:

image-20240113145126700

实际开发中,选择Browser Rotuer还是Hash Router?

1、如果要兼容低版本的浏览器,则推荐使用 Hash Router。

2、否则,建议使用 Browser Router,因为它功能更强大,能够使用浏览器的历史对象管理路由信息。

1.4创建根路由的组件

1、创建src/components下的root.tsx模块

2、复制/粘贴如下的组件代码到src/routes/root.tsx中:

3、修改src/main.tsx文件,导入Root组件,并渲染为/路径的element节点:

效果:

image-20240113150227181

注意:如果您想用@指向src/目录,需要配置项目下的vite.config.tstsconfig.json文件。

1.5补充:配置@路径

  1. 安装 node 的类型声明:

为什么需要配置@路径提示?

因为这样很方便,在引入外部文件的时候,一般使用的是相对路径,但是相对路径不是明确,接手的人不容易找到文件所在位置。

如果是按照src目录下来找,就会很明确,依次查找文件即可。

image-20231127092012401

项目创建后,默认使用的就是相对路径。

  1. 配置 vite.config.ts 文件:

image-20231127092236783

只需要添加代码即可,原有的代码不需要动。

  1. 配置 tsconfig.json 文件,在 compilerOptions 节点下,新增 "baseUrl": ".""paths": { "@/*": [ "src/*" ] } 两项:

这样,就可以使用@来表示/src/目录了。

image-20240113150758756

启动项目,没有问题。

2.路由的基本用法

2.1处理Not Found错误

目前,点击左侧任意的导航链接,会导航到 react router 默认提供的错误页。

为了使用户体验更好,我们在进行项目开发时,一般都会自定义错误页。

1、创建src/error-page.tsx错误页,对应的组件如下:

 

2、把<ErrorPage />设置为根路由的errorElement属性:

此时,点击左侧任意的导航链接,当无法匹配任何路由时,就会展示根路由的errorElement属性所匹配的<ErrorPage />错误页。

效果:

3、为了能在ErrorPage中展示具体的错误消息,我们需要使用react-router-dom提供的useRouteErrorhook:

效果:

2.2渲染联系人相关的路由组件

1、在src/components目录下新建contact.tsx模块,并把对应的UI结构复制/粘贴进去:

2、修改src/main.tsx中的代码,导入Contact组件:

并添加路由配置如下:

效果:

 

2.3基于嵌套路由渲染Contact组件

需求:我们希望把 Contact 组件渲染到 Root 组件中右侧的位置,而不是像上面那样,是整个页面。具体位置是:把 Contact 组件渲染到Root组件的 id 为 detail 的 div 中。

1、修改src/main.tsx中的代码,通过 children 属性把contacts/:id的Route定义为/Route的子路由:

2、修改src/components/root.tsx中的代码,从react-router-dom中导入Outlet组件(占位符):

并把它放到id属性为detail的div中:

效果:

 

2.4使用Link代替普通的a链接

在上面的例子中,可以看到跳转链接的时候,浏览器的刷新按钮会刷新,那么在现代前端框架下,我们都是使用无刷新的路由跳转,所以可以使用Link代替普通的a链接。

1、在src/components/root.tsx中导入Link组件

2、使用Link代替普通的a链接

效果:

可以看到,无论路由怎么切换,浏览器的刷新按钮都不会动。

3.路由组件的数据操作

这部分的内容是非常新的内容,无论是vue还是react尚硅谷版本,都没有这个内容,以往学习路由的时候,只是要求页面能够跳转即可,数据就在vue生命周期或者react生命周期中处理,在vue中这样写是没有问题的,但是在react中,特别是使用hooks的时候,这样写就会非常麻烦,重复代码也很多,所以就出现了data APIs。多学几遍。

了解路由中的data APIs:

image-20240113163001362

基本用法

1、在src/components/root.tsx中,Root组件平级的位置定义并暴露出loader函数:

image-20240113164102245

2、在src/main.tsx中,导入并重命名loader,并挂载到路由上:

效果:

结果说明:在进入root组件之前,会先调用loader函数,再渲染组件。

3、使用react-router-dom提供的useLoaderData这个hook来获取loader函数返回的数据:

image-20240113165637175

效果:

3.1加载初始数据

1、修改src/components/root.tsx模块,先从src/contacts.ts中按需导入数据API:

再创建名为的函数,并向外按需导出:

2、修改src/main.tsx模块,先从src/components/root.tsx按需导入loader函数:

再给对应的Route添加选项:

3、在src/components/root.tsx组件中,使用useLoaderData获取loader返回的数据,并根据返回的数据渲染组件:

效果:

3.2创建联系人

需求:在进行CURD操作时,把操作提交到action中进行处理。

可以预料到,如果一个大型项目中,使用Form来提交操作的话,会导致router非常臃肿,虽然可以通过拆分的方式来缓解,但是这种编码方式是一种全新的挑战,真正做项目之前看一下别人的案例,做就行了。

1、这里就需要使用到react-router-dom提供的Form组件,将src/components/root.tsx中原来的form组件换为Form组件。

2、在src/components/root.tsx中,创建并暴露出一个action函数:

3、在src/main.tsx中,导入并将配置根路由的action属性:

效果:

4、使用contacts.ts中定义好的新增方法,在定义的action函数中执行这个方法:

点击New按钮,看一下效果:

可以看到新增了两个no name的contacts,并且新增之后会自动渲染到sidebar上面,为什么呢?

因为在action执行完成之后,会重新执行loader函数,并重新渲染组件。口说无凭,在定义的loader和action函数里面输出一些东西,看一下效果:

注意:action函数中是否需要 return 数据?

上面点击按钮后,实际上渲染的是从loader中拿到的数据,并不是从action中拿到的数据,那action中return出去的数据,到哪去了呢?

实际上return到了路由中action属性所在的path指向的组件中。使用react-router-dom提供的useActionData就可以拿到这个数据。

测试一下:

效果:

但其实在action函数中,如果不是特殊情况要从useActionData()里面获取数据的话,是没有必要在action函数中return数据出去的,所以一般的action函数写成这样:

下面说明一下action的执行流程:

image-20240113202749342

image-20240113203040822

3.3在loader中访问params参数

目前,contact.tsx组件中的联系人信息是写死的静态数据。当使用路由加载此组件时,我们应该在loader中获取联系人的id,并动态请求联系人的数据进行渲染。

1、修改src/components/contact.tsx组件,从react-router-dom中按需导入LoaderFunctionArgs这个TS类型,它表示loader函数的形参类型:

然后,从src/contacts.ts中导入需要的数据API:

最后,创建并向外导出名为的函数:

2、修改src/main.tsx组件,从src/routes/contact.tsx中按需导入loader后重命名为contactLoader

contactLoader挂载给path为contacts/:id的Route:

3、修改src/components/contacts.tsx组件,从react-router-dom中按需导入useLoaderData这个hook:

修改Contact组件,将组件内部写死的静态数据,替换为useLoaderData的调用,从而根据id获取联系人的信息:

效果:

从上面的例子可以看到,loader和路由是对应的关系,和对应的组件也是一一对应的关系,那么虽然使用的都是useLoaderData(),拿到的数据却是准确的,这一点要理解清楚。

3.4更新联系人信息

3.4.1绘制更新联系人信息的路由组件

1、在src/components目录下新建edit.tsx组件,代码如下:

2、在src/main.tsx中新增路由规则:

3、点击详情页面的edit按钮,就可以跳转到相应的页面了:

image-20240113213840215

3.4.2以FormData格式更新联系人的信息

需求:点击编辑页面的Save按钮,可以保存联系人的信息,并在保存成功后,跳转到原来的界面。

解决方法:在action中获取表单数据并修改联系人信息。

1、在src/components/edit.tsx中新增action函数:

并挂载到路由上:

2、在EditContact组件中,使用的是react-router-dom提供的Form组件,所以提交的数据会被action接收到:

image-20240114101325956

先引入action参数的TS类型:

然后在action中输出参数看一下:

注意:

这里的Form表单并没有指定action,通过上面学习的知识,我可以知道action实际指向了组件在router里面注册的path,这一点很好理解。

但是Form里面有两个button,怎么确定点击Save按钮是触发表格提交的,而点击Cancel不会触发表格的提交呢?其实button的type属性就规定了能否触发,如果type="submit"就会触发。

看一下输出即可知道,request本身上并不能直接得到表单数据,但是在它的原型上有一个formData函数,通过这个函数可以得到表单的数据,测试一下:

可以看到,request.formData()返回的是Promise对象,所以要用async...await来使用。

由于提交数据的方法接收的是对象,而不是formData类型,所以需要将formData数据转为对象,使用Object.fromEntries()方法:

可以看到,已经获取到想要的数据了。

3、从src/contacts.ts中引入更新联系人信息的方法:

在action函数中提交数据,更新数据的方法有两个参数,第一个参数是id,所以需要从params获取到id:

可以看一下ActionFunctionArgs类型,有没有params参数?

image-20240114103843169

image-20240114103918510

是有的,可以使用。

可以看到,在保存数据之后,sidebar里面的信息也自动更新了,这再次说明了action的执行顺序。

4、使用react-router-dom提供的redirect方法,在提交完数据之后,返回原页面:

可以看到,在点击Save更新成功之后,就自动跳转到原页面了。

3.4.3点击新增按钮时立即重定向到Edit组件

在新增时,可以拿到返回值里面的id,使用react-router-dom提供的redirect,重定向到Edit组件。

image-20240114105345501

src/components/root.tsx中,修改action函数,重定向到Edit组件:

可以看到,整个流程都顺畅了。

4.美化UI状态

4.1美化激活链接的样式

随着左侧菜单中数据的增多,我们很难准确分辨出哪个链接被点击了,因此,我们可以使用NavLink代替Link组件。

因为NavLink组件可以使用className属性控制链接的样式。

1、在src/components/root.tsx中,从react-router-dom中按需导入NavLink组件:

2、使用NavLink替换src/components/root.tsx中的Link组件:

3、请注意:正在渲染中的路由对应的链接会添加pending类名,被选中的NavLink默认会添加active类名。

请注意看右侧元素上的class类名,从pending类名转到了active类名。

4、使用NavLinkclassName属性,自定义active和pending样式。

src/index.css中设置样式:

4.2展示路由pending的状态

在进行路由切换时,会先执行下个路由的loader,此时当前路由的组件依然被展示在页面上。为了给用户一个明显的提示,表明当前组件不是最新的,我们需要用到useNavigation这个hook。

useNavigation会拿到路由的状态,即navigation.state,它有三个值:

因此,我们可以判断是否处于loading状态,从而使用CSS样式提示用户当前处理路由的pending状态:

1、修改src/components/root.tsx组件,从react-router-dom中按需导入useNavigationhook:

2、在Root组件中获取路由的信息:

3、给id属性为detail的div添加className如下:

可以看到一个过渡的泛白效果。

4.3删除数据

在用户信息页面中,我们预留了一个Delete按钮。并提供了method="post"action="destroy"onSubmit事件处理函数。

1、在src/components/下创建delete.tsx模块,并声明如下的action:

2、在src/main.tsx中引入action,并注册删除功能对应的路由:

image-20240115201453130

3、在src/components/delete.tsx中导入参数的TS类型,使用deleteContact方法来删除联系人:

效果:

4.4处理上下文的错误

1、如果我们在{path:"contact/:id/destroy",action:deleteContactAction}的action中向外抛出一个错误:

2、这个错误最终会被path:"/"errorElement捕获并处理,因此页面上显示的<ErrorPage />组件。此时用户无法进行其它操作,只能点击浏览器的后退按钮。

3、为了提高用户的体验,我们可以为每一个Route都提供一个专属的errorElement(虽然这不是必须的):

4.5索引路由

当用户首次进入App时,会看到右侧面板一片空白,为了解决此问题,我们可以使用react-router-dom的index Route。

1、在src/components/下新建index.tsx组件:

2、改造src/main.tsx组件中的代码,导入src/components/index.tsx组件:

path:"/"的Route添加index子路由:

4.6后退到上一个页面

在点击详情页的Cancel时,想返回上一个页面,但是现在没有任何效果,可以使用react-router-dom提供的useNavigate来进行跳转。

1、改造src/components/edit.tsx组件,先按需导入useNavigatehook如下:

2、在EditContact组件中,调用useNavigatehook:

3、为<button type="button">Cancel</button>按钮绑定点击事件处理函数:

5.搜索相关的功能

5.1根据客户端路由获取GET提交的数据

1、改造src/components/root.tsx组件中的代码,将搜索框外层的<form>替换为<Form>组件:

image-20240115211704618

这样用户在搜索框中触发搜索之后,会在url上拼接上query参数,并且会触发组件的重新渲染,从而可以在loader函数里面获取到url上面的query参数。

2、在loader函数里面,获取query参数,并查询:

在这里其实我有一个疑问,就是搜索框所在的Form组件不是对应action函数吗?为什么这里的搜索框的Form提交之后,不是在action里面进行处理呢?

首先确认搜索框所在的Form组件触发提交之后,action函数会不会执行?通过在action函数里面打印内容,答案是不会触发。

那么第二点就可以回答了,因为本身就不会触发action函数,那就不能在action函数里面处理了。

那么第一点,Form组件一定是对应action函数吗?不一定,因为搜索框所在的Form组件没有提供method属性,所以默认就是GET方法,而GET方法是不能触发action函数的:https://reactrouter.com/en/main/route/action

image-20240126145513781

那么最后一个疑问:如果这个界面有多个操作,我要触发多个action函数,该怎么办?

很简单,就像Delete按钮一样,一个操作对应一个路由地址和action函数,多加几个路由就行了。

5.2保证URL和Form状态的同步

在搜索时,如果刷新网页,搜索框中的关键词会丢失,但搜索结果还在:

为什么刷新之后,搜索结果还在呢?因为刷新之后,url地址没有变化,loader会解析里面的查询参数,所以搜索结果还在。

为什么搜索框里面的值丢失了呢?因为刷新后,url地址指向了根路由,根组件Root会重新渲染,而搜索框里面并没有保存值,所以值会丢失。

5.2.1刷新页面时,同步url与搜索框的值

可以为搜索框指定默认值来同步url与搜索框的值。

1、在loader函数中,将从url中获取到的搜索值返回出去:

2、使用useLoaderData来接收传递过来的搜索值,并赋值给搜索框:

效果:

5.2.2点击后退按钮时,同步url与搜索的值

在点击浏览器的前进后退按钮时,URL和搜索框里面的值没有同步,但搜索结果还是正常的:

说明input框里面defaultValue拿到的q值,是最新的。原因是input框里面defaultValue值的改变,并不能触发react组件的重新渲染,所以值没有及时更新。

解决方法一:

为input框添加key值,赋值为q,react对key值的更新,会监听并触发组件重新渲染:

解决方法二:

使用useEffect监听q值的变化,为input赋值:

效果:

解决方法一是老师推荐的方法,使用很简单。解决方法二是react-router-dom官方文档的tutorial里面的写法,理解起来很简单。

5.3在文本框onChange时触发Form提交

在搜索框输入文字后,点击enter键才能触发Form的提交,但是我们想搜索框里面边输入边搜索,那么需要为input绑定onChange事件,并且用到react-router-dom的useSubmit方法来触发Form的提交。

1、在src/components/root.tsx中引入useSubmit方法:

2、在input的onChange事件中使用useSubmit方法:

 

效果:

这里有一个问题,就是搜索框输入之后,搜索框是返回最新结果了,但是搜索框失去了焦点,如果用户想输入多个字符来搜索,这样肯定是非常麻烦的,因此必须要在搜索框重新渲染之后,自动聚焦。

可以在input搜索框上设置autoFocus属性即可自动聚焦。但还有个问题,就是中文输入法下面,如果输入中文,不等输入完成,就会自动触发搜索,这个问题还没有找到答案。

问题:

 

这个问题的可能原因是没有节流搜索,导致页面渲染出错。加上节流试试看。

5.4添加搜索时的loading效果

 

 

 

5.5管理搜索产生的历史栈

 

 

 

6.其它功能以及优化

6.1在不产生导航的前提下修改数据

这里的“不产生导航”是什么意思???

之前在创建、修改、删除时,都创建了相应的action,分别是RootActionEditContactActionDeleteContactAction,这些action都对应了相应的router的path,在触发的时候,,,,,写不下去了

 

像delete按钮那样,就是写了一个导航地址,然后配置action函数来到到目的。这里的意思是在不写导航地址的前提下直接写一个action来修改数据。

 

1、在src/components/contact.tsx里面引入useFetcher

2、将Favorite组件的Form改为fetcher.Form

3、编写action函数,并引入到src/main.tsx文件中:

4、在src/components/root.tsx中,添加favorite状态的展示:

 

效果:

6.2优化UI对数据的响应

在上个案例中,当点击favorite按钮之后,要等到数据真实更新之后,重新渲染才能得到点击之后的效果。为了给用户更好的反馈体验,当点击favorite按钮之后,可以直接将五角星的状态进行更改,即使数据最终没有更新,那么在真实数据返回之后,也可以更改过来。

可以使用fetcher.state来显示loading状态,像navigation.state一样的用法。但是这里可以使用fetcher.formData来获取到form data提交给action的值,立刻更新五角星的状态。

 

效果:

 

代码很简单,但这里其实涉及到很重要的流程问题,一定要弄清楚:我点击favorite按钮之后,到底是怎么执行的?

我在组件内、loader函数、action函数里面都输出一下,看执行顺序到底是什么样的?

可以看到,组件在action函数触发之后,重新渲染了一次,然后在loader之后再次重新渲染,最后拿到最新的值时,又渲染一次,总共渲染了三次。

这是官方的说明:

image-20240124151709565

6.3查无数据的处理

在url中访问并不存在的contact,会返回这样的界面:

image-20240124155129959

这个错误被errorElement捕获并处理,但是有时候可以让界面的信息更明确一些,如果在loader函数或者action函数中有任何错误,都可以使用throw来抛出错误,这样react router会捕获到,从而渲染errorElement的组件:

image-20240124155721319

 

 

6.4无path的Route

上面的案例中,错误页占满了整个页面,这是因为错误最终被根路由的errorElement捕获到了,如果想要在<Outlet />显示的范围内显示错误页,则需要为每个子路由配置errorElement属性,这样虽然可以做到很好的效果,但是对于一些共同的错误页内容来说,配置起来就很麻烦:

有没有办法能够一次性为多个子路由配置错误页呢?可以,children属性数组的值需要改写:

 

6.5jsx形式的Route

很多人喜欢将路由写成jsx的形式,这是react-router-dom之前版本的写法,新版本也支持,需要使用createRoutesFromElements来创建,并使用Route标签。

 

 

6.6把路由配置抽离到独立的组件中

这个操作也很简单,只需要把router相关的代码全部放到一个组件中,然后在main.tsx中引入并使用即可。

1、在src/components中新建router.tsx文件,将路由配置代码全部放到这个组件中。

2、在main.tsx中引入路由配置:

效果OK。

 

 

 

 

自己的一些疑问:

有一个问题:在搜索后的sidebar里面,点击某一项,那么sidebar就会重新渲染,搜索框里面的内容也丢失了。

根本原因是q这个值丢失了,由于q是从url上获取的,那么点击sidebar的某一项导航后,跳转到了另外一个url地址,肯定不能从这个url上获取搜索条件,那么其实root.tsx里面获取的q在真实项目中还需要加判断条件,这里先不管。

那么只需要把q这个值放到zustand或者redux里面就行了,用的时候就拿。

改写loader函数:

这样就好了:

但还是有问题,当搜索框清空之后,搜索结果不对,原因是搜索框清空之后,存储的数据没有清空,需要重新设计判断条件:

 

 


在root.tsx中,Root的代码很复杂,那么能不能把获取contacts数据渲染组件的代码,放到一个单独的组件中呢?这样还可以使用React.memo来优化,如果数据没有变化,那么组件就不用重新渲染。

在main.tsx文件中,注册的路由是这样的:

element属性的值是:<Root />,loader和action都在同层级定义,是不是意味着只能在Root组件的函数代码里面使用loader和action相关的api呢?可以这么说,但是如果一个子组件本身就是要用在这个组件里面的,那当然本身就属于这个组件的一部分,使用loader和action相关的api也是没有问题的。

可以看到功能是正常的,但是memo并没有起作用,是什么原因呢?


目前所做的项目,路由还是有些复杂的,react中该怎么做呢?

image-20240131140847687

image-20240131140902232

首先要搞清楚菜单的组成,这个问题不先搞清楚,越做越糊涂。

菜单是由真正的路由和目录组成的,像上图中的活动管理就是目录名称,而里面的“活动信息”、“活动报名”、“活动报名申请”、“活动报名审核”这些才是真正的路由。

这里的菜单只有两层结构,如果有更多层,也是这样的组成。

那么点击目录名称,UI交互就只有收起和展开里面的内容,不需要进行高亮显示、不会进行跳转。而点击路由,就会有高亮显示,并且会跳转到相应的页面。

那么真正的路由就可以使用NavLink来包裹(NavLink本身就可以设置高亮效果,都不需要另外设置了,这很好)。目录就可以使用div标签来表示,div上绑定事件即可,也很简单。这里我想说,如果是直接使用固定的路由标签来写,会很复杂,团队开发时更改也很难,所以还是需要做一个路由的维护界面,将路由数据改造成js对象,然后在真正生成路由的时候,根据这个数据来渲染生成即可,这样的效果是最好的。下面是vue项目的路由例子,很多地方可以参考,比如说hasChildren为true、urlAddress为空的话,就表示这是一个目录,就渲染为div,别的情况就渲染为NavLink。

可以自己实现一下,就用上面的模拟数据渲染一下,一定要做啊。


搞懂了路由的嵌套,就可以做很复杂的项目。

在学习vue-router的时候,我只学会了router.js里面该怎么写,但是具体的路由插槽却一直没有搞懂,所以自己创建vue项目就有些问题,比如说项目左边是菜单栏,如何根据左侧菜单栏,在右侧显示不同的页面呢?刚开始的时候根本就没有搞懂,我以为路由跳转了,并且在router.js里面该路由下注册了这个组件,然后就一定会显示这个组件。我忘记了写这个路由组件应该在哪里显示呢?这时候就要用到路由插槽,是要写在组件<template></template>里面的,在vue中就是<Router-view />,比如说根path对应的是"/",组件是root.vue,那么里面就要写成左边部分和右边部分。左边部分基本上可以固定写元素,但是右边部分就要加上<router-view />的插槽,这样根path下面的children渲染的时候,就可以在右侧进行显示。

react中也是同样的思路,如果想要通过路由跳转来切换组件的展示,那么除了在router.js里面进行定义之外,还需要在具体的父组件中的某个位置加上<Outlet />标签,作为路由插槽。下面以老师的react-article-admin-template项目来说明:

router.ts文件里面:

image-20240325111534158

views/root/root.tsx文件里面:

image-20240325111625467

组件的切换,不一定使用路由的跳转来实现,一般都是大的组件用路由切换,如果是小的组件切换,完全可以用{flag && xxComponent}来实现切换,vue中就是v-if或者v-show来实现。

路由插槽真的是一个很好的发明,在框架层面做到了路由跳转和监听渲染,用户只需要使用,就可以实现复杂的功能。